Skip to content

Attachment package refactor #311

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 23 commits into
base: main
Choose a base branch
from

Conversation

dean-journeyapps
Copy link
Contributor

No description provided.

Copy link
Contributor

@simolus3 simolus3 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some more comments from a quick look.

Also, I assume supabase-todolist-new-attachments was copied from the old one? For a review it would be easier if the changes were in the same project so I can see what has changed. But that can happen in the end.

Copy link
Contributor

@simolus3 simolus3 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few more comments, most of them pretty minor again :)

Comment on lines 16 to 20
import 'dart:async';
import 'dart:io';
import 'dart:typed_data';
import 'package:path/path.dart' as p;
import '../abstractions/local_storage.dart';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Typically one inserts an empty line between dart: and package: imports and then another one before local imports. All the effective Dart samples do that, but it's not spelled out explicitly. I've added a suggestion for this here, it affects other files as well.

Suggested change
import 'dart:async';
import 'dart:io';
import 'dart:typed_data';
import 'package:path/path.dart' as p;
import '../abstractions/local_storage.dart';
import 'dart:async';
import 'dart:io';
import 'dart:typed_data';
import 'package:path/path.dart' as p;
import '../abstractions/local_storage.dart';

attachment.copyWith(state: AttachmentState.archived),
);
}
break;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You don't need this break anymore in recent Dart versions.

handleData: (data, sink) {
sink.add(data);
// Simple throttle implementation - just delay the next event
Future.delayed(throttle);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This future is unawaited and will not actually throttle anything.

I think it would be easier to not throttle the stream here and instead add a duration parameter to watchActiveAttachments. We can forward that to db.watch() instead of duplicating the throttle implementation here. That means we wouldn't be throttling manualTriggers anymore, but I think that's fine.

Copy link
Contributor

@simolus3 simolus3 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The implementation looks good now, I have a few nits on types. I also think we should strive to not introduce classes / interfaces we don't strictly need, especially if they're publicly exported. Also more generally:

  • Please run dart format on the package. I also recommend enabling format on save.
  • I think a few more tests with an actual database would be nice to have, you can take a look at the Kotlin tests for attachments for inspiration. We can test on native platforms only in the beginning, you can probably copy something like this to set up databases. But it's still a bit tricky, let me know if you need help with this. I can also push a basic scaffold for tests to this branch.

@@ -0,0 +1,10 @@
# This file tracks properties of this Flutter project.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given that this package is no longer a Flutter project, I think we can delete this file.

/// Default exports for native platforms (dart:io). For web, use 'common.dart'.
export 'common.dart';
export 'src/storage/io_local_storage.dart';
export 'src/attachment_queue_service.dart';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is exported by common.dart now and can be removed here.

///
/// [id]: The ID of the attachment to delete.
/// [tx]: The database context to use for the operation.
Future<void> deleteAttachment(String id, dynamic context);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This shouldn't be dynamic, can this be a SqliteWriteContext?

///
/// [id]: The ID of the attachment to delete.
/// [tx]: The database context to use for the operation.
Future<void> deleteAttachment(String id, dynamic context);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also this function isn't called at all, I assume that should happen in SyncingService.deleteAttachment?

/// [attachment]: The attachment to upsert.
/// [context]: The database transaction/context to use for the operation.
/// Returns the upserted [Attachment].
Future<Attachment> upsertAttachment(Attachment attachment, dynamic context);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

context should likely be a SqliteWriteContext here as well?

@@ -0,0 +1,34 @@
import 'dart:typed_data';

abstract class AbstractLocalStorageAdapter {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's make this an interface class and just name it LocalStorageAdapter:

Suggested change
abstract class AbstractLocalStorageAdapter {
abstract interface class LocalStorageAdapter {

Comment on lines 191 to 193
return fileExtension != null
? '$attachmentId.$fileExtension'
: '$attachmentId.dat';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return fileExtension != null
? '$attachmentId.$fileExtension'
: '$attachmentId.dat';
return '$attachmentId.${fileExtension ?? 'dat'}';

Comment on lines 208 to 212
.cast<Attachment?>()
.firstWhere(
(a) => a != null && a.id == item.id,
orElse: () => null,
);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think a more intuitive way to express this without the cast() may be

Suggested change
.cast<Attachment?>()
.firstWhere(
(a) => a != null && a.id == item.id,
orElse: () => null,
);
.where((a) => a.id == item.id)
.firstOrNull;

required String mediaType,
String? fileExtension,
String? metaData,
required Future<void> Function(dynamic context, Attachment attachment)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
required Future<void> Function(dynamic context, Attachment attachment)
required Future<void> Function(SqliteWriteContext context, Attachment attachment)

/// The default implementation assumes the attachment record already exists locally.
Future<Attachment> deleteFile({
required String attachmentId,
required Future<void> Function(dynamic context, Attachment attachment)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
required Future<void> Function(dynamic context, Attachment attachment)
required Future<void> Function(SqliteWriteContext context, Attachment attachment)

await attachmentQueue.deleteFile(
attachmentId: todo.photoId!,
updateHook: (context, attachment) async {
// await context.execute("UPDATE todos SET photo_id = NULL WHERE id = ?", [todo.id]);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you should probably still do the un-assignment here, is uncommenting this perhaps a TODO?

dean-journeyapps and others added 20 commits August 19, 2025 15:38
…handling in powersync_attachments_stream package and added unit tests
…_stream package. Removed unused imports, improved error handling during file uploads and downloads, and enhanced and renamed logging for better traceability of attachment operations.
…hods, improving clarity on attachment lifecycle management, state handling, and local storage operations.
…hments package implementation to supabase-todolist demo.
… path for local storage in the powersync_attachments_stream package. Simplified the attachment watching logic. Updated README for clarity on schema and attachment states.
… classes to utilize the new interface and adjusted README for consistency.
… package. Updated attachment queue initialization to use localStorage, and enhanced README for clarity on storage handling. Added tests for edge cases and robustness in local storage operations.
…consistency. Removed outdated comment regarding attachments directory in attachment queue service documentation.
…dated photo capture widget to save attachments as byte data instead of streams, and modified related methods in the attachment queue service to accept byte data. Updated tests to reflect changes in data handling.
…comments, replacing multiple error handler parameters with a single optional error handler for sync-related errors.
@simolus3 simolus3 force-pushed the attachment-package-refactor branch from fa2f16e to 248d748 Compare August 19, 2025 14:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants